[id].vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. <template>
  2. <div class="flex h-screen">
  3. <div
  4. class="w-80 bg-white py-6 relative border-l border-gray-100 flex flex-col"
  5. >
  6. <workflow-edit-block
  7. v-if="state.isEditBlock"
  8. :data="state.blockData"
  9. @update="updateBlockData"
  10. @close="(state.isEditBlock = false), (state.blockData = {})"
  11. />
  12. <workflow-details-card
  13. v-else
  14. :workflow="workflow"
  15. :data-changed="state.isDataChanged"
  16. @save="saveWorkflow"
  17. @export="exportWorkflow"
  18. @execute="executeWorkflow"
  19. @update="updateWorkflow"
  20. @showDataColumns="state.showDataColumnsModal = true"
  21. @showSettings="state.showSettings = true"
  22. @rename="renameWorkflow"
  23. @delete="deleteWorkflow"
  24. />
  25. </div>
  26. <div class="flex-1 relative overflow-auto">
  27. <div class="absolute px-3 rounded-lg bg-white z-10 left-0 m-4 top-0">
  28. <ui-tabs v-model="activeTab" class="border-none h-full space-x-1">
  29. <ui-tab value="editor">Editor</ui-tab>
  30. <ui-tab value="logs">Logs</ui-tab>
  31. <ui-tab value="running" class="flex items-center">
  32. Running
  33. <span
  34. v-if="workflowState.length > 0"
  35. class="
  36. ml-2
  37. p-1
  38. text-center
  39. inline-block
  40. text-xs
  41. rounded-full
  42. bg-black
  43. text-white
  44. "
  45. style="min-width: 25px"
  46. >
  47. {{ workflowState.length }}
  48. </span>
  49. </ui-tab>
  50. </ui-tabs>
  51. </div>
  52. <keep-alive>
  53. <workflow-builder
  54. v-if="activeTab === 'editor'"
  55. class="h-full w-full"
  56. :data="workflow.drawflow"
  57. @load="editor = $event"
  58. @deleteBlock="deleteBlock"
  59. />
  60. <div v-else class="container pb-4 mt-24 px-4">
  61. <template v-if="activeTab === 'logs'">
  62. <div v-if="logs.length === 0" class="text-center">
  63. <img
  64. src="@/assets/svg/files-and-folder.svg"
  65. class="mx-auto max-w-sm"
  66. />
  67. <p class="text-xl font-semibold">No data to show</p>
  68. </div>
  69. <shared-logs-table :logs="logs" class="w-full">
  70. <template #item-append="{ log: itemLog }">
  71. <td class="text-right">
  72. <v-remixicon
  73. name="riDeleteBin7Line"
  74. class="inline-block text-red-500 cursor-pointer"
  75. @click="deleteLog(itemLog.id)"
  76. />
  77. </td>
  78. </template>
  79. </shared-logs-table>
  80. </template>
  81. <template v-else-if="activeTab === 'running'">
  82. <div v-if="workflowState.length === 0" class="text-center">
  83. <img
  84. src="@/assets/svg/files-and-folder.svg"
  85. class="mx-auto max-w-sm"
  86. />
  87. <p class="text-xl font-semibold">No data to show</p>
  88. </div>
  89. <div class="grid grid-cols-2 gap-4">
  90. <shared-workflow-state
  91. v-for="item in workflowState"
  92. :id="item.id"
  93. :key="item.id"
  94. :state="item.state"
  95. />
  96. </div>
  97. </template>
  98. </div>
  99. </keep-alive>
  100. </div>
  101. </div>
  102. <ui-modal v-model="state.showDataColumnsModal" content-class="max-w-xl">
  103. <template #header>Data columns</template>
  104. <workflow-data-columns
  105. v-bind="{ workflow }"
  106. @update="updateWorkflow"
  107. @close="state.showDataColumnsModal = false"
  108. />
  109. </ui-modal>
  110. <ui-modal v-model="state.showSettings">
  111. <template #header>Workflow settings</template>
  112. <workflow-settings v-bind="{ workflow }" @update="updateWorkflow" />
  113. </ui-modal>
  114. </template>
  115. <script setup>
  116. /* eslint-disable consistent-return */
  117. import {
  118. computed,
  119. reactive,
  120. shallowRef,
  121. provide,
  122. onMounted,
  123. onUnmounted,
  124. } from 'vue';
  125. import { useStore } from 'vuex';
  126. import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
  127. import browser from 'webextension-polyfill';
  128. import emitter from 'tiny-emitter/instance';
  129. import { sendMessage } from '@/utils/message';
  130. import { debounce } from '@/utils/helper';
  131. import { useDialog } from '@/composable/dialog';
  132. import { exportWorkflow } from '@/utils/workflow-data';
  133. import Log from '@/models/log';
  134. import Workflow from '@/models/workflow';
  135. import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
  136. import WorkflowSettings from '@/components/newtab/workflow/WorkflowSettings.vue';
  137. import WorkflowEditBlock from '@/components/newtab/workflow/WorkflowEditBlock.vue';
  138. import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
  139. import WorkflowDataColumns from '@/components/newtab/workflow/WorkflowDataColumns.vue';
  140. import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
  141. import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
  142. const store = useStore();
  143. const route = useRoute();
  144. const router = useRouter();
  145. const dialog = useDialog();
  146. const workflowId = route.params.id;
  147. const editor = shallowRef(null);
  148. const activeTab = shallowRef('editor');
  149. const state = reactive({
  150. blockData: {},
  151. isEditBlock: false,
  152. showSettings: false,
  153. isDataChanged: false,
  154. showDataColumnsModal: false,
  155. });
  156. const workflowState = computed(() =>
  157. store.getters.getWorkflowState(workflowId)
  158. );
  159. const workflow = computed(() => Workflow.find(workflowId) || {});
  160. const logs = computed(() =>
  161. Log.query().where('workflowId', workflowId).orderBy('startedAt', 'desc').get()
  162. );
  163. const updateBlockData = debounce((data) => {
  164. state.blockData.data = data;
  165. state.isDataChanged = true;
  166. editor.value.updateNodeDataFromId(state.blockData.blockId, data);
  167. const inputEl = document.querySelector(
  168. `#node-${state.blockData.blockId} input.trigger`
  169. );
  170. if (inputEl) inputEl.dispatchEvent(new Event('change'));
  171. }, 250);
  172. function deleteLog(logId) {
  173. Log.delete(logId).then(() => {
  174. store.dispatch('saveToStorage', 'logs');
  175. });
  176. }
  177. function deleteBlock(id) {
  178. if (state.isEditBlock && state.blockData.blockId === id) {
  179. state.isEditBlock = false;
  180. state.blockData = {};
  181. }
  182. state.isDataChanged = true;
  183. }
  184. function updateWorkflow(data) {
  185. return Workflow.update({
  186. where: workflowId,
  187. data,
  188. });
  189. }
  190. async function handleWorkflowTrigger({ data }) {
  191. try {
  192. const workflowAlarm = await browser.alarms.get(workflowId);
  193. const { visitWebTriggers, shortcuts } = await browser.storage.local.get([
  194. 'visitWebTriggers',
  195. 'shortcuts',
  196. ]);
  197. let visitWebTriggerIndex = visitWebTriggers.findIndex(
  198. (item) => item.id === workflowId
  199. );
  200. const keyboardShortcuts = Array.isArray(shortcuts) ? {} : shortcuts || {};
  201. delete keyboardShortcuts[workflowId];
  202. if (workflowAlarm) await browser.alarms.clear(workflowId);
  203. if (visitWebTriggerIndex !== -1) {
  204. visitWebTriggers.splice(visitWebTriggerIndex, 1);
  205. visitWebTriggerIndex = -1;
  206. }
  207. await browser.storage.local.set({
  208. visitWebTriggers,
  209. shortcuts: keyboardShortcuts,
  210. });
  211. if (['date', 'interval'].includes(data.type)) {
  212. let alarmInfo;
  213. if (data.type === 'date') {
  214. alarmInfo = {
  215. when: data.date ? new Date(data.date).getTime() : Date.now() + 60000,
  216. };
  217. } else {
  218. alarmInfo = {
  219. periodInMinutes: data.interval,
  220. };
  221. if (data.delay > 0) alarmInfo.delayInMinutes = data.delay;
  222. }
  223. if (alarmInfo) await browser.alarms.create(workflowId, alarmInfo);
  224. } else if (data.type === 'visit-web' && data.url.trim() !== '') {
  225. const payload = {
  226. id: workflowId,
  227. url: data.url,
  228. isRegex: data.isUrlRegex,
  229. };
  230. if (visitWebTriggerIndex === -1) {
  231. visitWebTriggers.unshift(payload);
  232. } else {
  233. visitWebTriggers[visitWebTriggerIndex] = payload;
  234. }
  235. await browser.storage.local.set({ visitWebTriggers });
  236. } else if (data.type === 'keyboard-shortcut') {
  237. keyboardShortcuts[workflowId] = data.shortcut;
  238. await browser.storage.local.set({ shortcuts: keyboardShortcuts });
  239. }
  240. } catch (error) {
  241. console.error(error);
  242. }
  243. }
  244. function saveWorkflow() {
  245. const data = editor.value.export();
  246. updateWorkflow({ drawflow: JSON.stringify(data) }).then(() => {
  247. const [triggerBlockId] = editor.value.getNodesFromName('trigger');
  248. if (triggerBlockId) {
  249. handleWorkflowTrigger(editor.value.getNodeFromId(triggerBlockId));
  250. }
  251. state.isDataChanged = false;
  252. });
  253. }
  254. function editBlock(data) {
  255. state.isEditBlock = true;
  256. state.blockData = data;
  257. }
  258. function executeWorkflow() {
  259. if (editor.value.getNodesFromName('trigger').length === 0) {
  260. /* eslint-disable-next-line */
  261. alert("Can't find a trigger block");
  262. return;
  263. }
  264. const payload = {
  265. ...workflow.value,
  266. drawflow: editor.value.export(),
  267. isTesting: state.isDataChanged,
  268. };
  269. sendMessage('workflow:execute', payload, 'background');
  270. }
  271. function handleEditorDataChanged() {
  272. state.isDataChanged = true;
  273. }
  274. function deleteWorkflow() {
  275. dialog.confirm({
  276. title: 'Delete workflow',
  277. okVariant: 'danger',
  278. body: `Are you sure you want to delete "${workflow.value.name}" workflow?`,
  279. onConfirm: () => {
  280. Workflow.delete(route.params.id).then(() => {
  281. router.replace('/workflows');
  282. });
  283. },
  284. });
  285. }
  286. function renameWorkflow() {
  287. dialog.prompt({
  288. title: 'Rename workflow',
  289. placeholder: 'Workflow name',
  290. okText: 'Rename',
  291. inputValue: workflow.value.name,
  292. onConfirm: (newName) => {
  293. Workflow.update({
  294. where: route.params.id,
  295. data: {
  296. name: newName,
  297. },
  298. });
  299. },
  300. });
  301. }
  302. provide('workflow', {
  303. data: workflow,
  304. updateWorkflow,
  305. /* eslint-disable-next-line */
  306. showDataColumnsModal: (show = true) => (state.showDataColumnsModal = show),
  307. });
  308. onBeforeRouteLeave(() => {
  309. if (!state.isDataChanged) return;
  310. const answer = window.confirm(
  311. 'Do you really want to leave? you have unsaved changes!'
  312. );
  313. if (!answer) return false;
  314. });
  315. onMounted(() => {
  316. const isWorkflowExists = Workflow.query().where('id', workflowId).exists();
  317. if (!isWorkflowExists) {
  318. router.push('/workflows');
  319. }
  320. window.onbeforeunload = () => {
  321. if (state.isDataChanged) {
  322. return 'Changes you made may not be saved.';
  323. }
  324. };
  325. emitter.on('editor:edit-block', editBlock);
  326. emitter.on('editor:data-changed', handleEditorDataChanged);
  327. });
  328. onUnmounted(() => {
  329. window.onbeforeunload = null;
  330. emitter.off('editor:edit-block', editBlock);
  331. emitter.off('editor:data-changed', handleEditorDataChanged);
  332. });
  333. </script>
  334. <style>
  335. .ghost-task {
  336. height: 40px;
  337. @apply bg-box-transparent;
  338. }
  339. .ghost-task:not(.workflow-task) * {
  340. display: none;
  341. }
  342. </style>